package main

import (
	"context"
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/energye/systray"
	"github.com/wailsapp/wails/v2/pkg/runtime"
	"golang.design/x/hotkey"
)

// FileNode represents a file or directory in the tree
type FileNode struct {
	Name     string      `json:"name"`
	Path     string      `json:"path"`
	IsDir    bool        `json:"isDir"`
	Size     int64       `json:"size"`
	ModTime  time.Time   `json:"modTime"`
	Children []*FileNode `json:"children,omitempty"`
}

// Config represents application configuration
type Config struct {
	TargetFolder string `json:"targetFolder"`
	DarkMode     bool   `json:"darkMode"`
	GlobalHotkey string `json:"globalHotkey"`
	AutoHide     bool   `json:"autoHide"`
	HideOnEdge   bool   `json:"hideOnEdge"`
}

// App struct
type App struct {
	ctx    context.Context
	config *Config
	globalHotkey *hotkey.Hotkey
	trayReady bool
	trayQuit  chan struct{}
}

// NewApp creates a new App application struct
func NewApp() *App {
	return &App{
		config: &Config{
			TargetFolder: getDesktopPath(),
			DarkMode:     false,
			GlobalHotkey: "Ctrl+Shift+F",
			AutoHide:     true,
			HideOnEdge:   true,
		},
	}
}

// startup is called when the app starts
func (a *App) startup(ctx context.Context) {
	a.ctx = ctx
	a.trayQuit = make(chan struct{})
	a.loadConfig()
	a.registerGlobalHotkey()
	a.initSystemTray()
}

// shutdown is called when the app is shutting down
func (a *App) shutdown(ctx context.Context) {
	// Unregister global hotkey
	if a.globalHotkey != nil {
		a.globalHotkey.Unregister()
		a.globalHotkey = nil
	}
	
	// Quit system tray
	if a.trayReady {
		close(a.trayQuit)
		systray.Quit()
	}
}

// GetFileTree returns the file tree for the specified directory
func (a *App) GetFileTree(path string) (*FileNode, error) {
	if path == "" {
		path = a.config.TargetFolder
	}

	// Try to load from cache first
	cachedTree, err := a.loadCachedFileTree(path)
	if err == nil && cachedTree != nil {
		return cachedTree, nil
	}

	// Build new tree and cache it
	tree, err := a.buildFileTree(path)
	if err != nil {
		return nil, err
	}

	// Save to cache
	a.saveCachedFileTree(path, tree)

	return tree, nil
}

// SearchFiles searches for files matching the query
func (a *App) SearchFiles(query string, path string) ([]*FileNode, error) {
	if path == "" {
		path = a.config.TargetFolder
	}

	var results []*FileNode
	query = strings.ToLower(query)

	err := filepath.WalkDir(path, func(filePath string, d fs.DirEntry, err error) error {
		if err != nil {
			return nil // Skip errors
		}

		fileName := strings.ToLower(d.Name())
		if strings.Contains(fileName, query) {
			info, err := d.Info()
			if err != nil {
				return nil
			}

			node := &FileNode{
				Name:    d.Name(),
				Path:    filePath,
				IsDir:   d.IsDir(),
				Size:    info.Size(),
				ModTime: info.ModTime(),
			}
			results = append(results, node)
		}

		return nil
	})

	return results, err
}

// GetConfig returns the current configuration
func (a *App) GetConfig() *Config {
	return a.config
}

// UpdateConfig updates the configuration
func (a *App) UpdateConfig(config *Config) error {
	a.config = config
	err := a.saveConfig()
	if err != nil {
		return err
	}
	
	// Re-register global hotkey with new configuration
	a.registerGlobalHotkey()
	return nil
}

// RefreshFileTree clears cache and rebuilds the file tree
func (a *App) RefreshFileTree(path string) (*FileNode, error) {
	if path == "" {
		path = a.config.TargetFolder
	}

	// Clear cache
	a.clearCachedFileTree(path)

	// Build new tree and cache it
	tree, err := a.buildFileTree(path)
	if err != nil {
		return nil, err
	}

	// Save to cache
	a.saveCachedFileTree(path, tree)

	return tree, nil
}

// SelectFolder opens a folder selection dialog
func (a *App) SelectFolder() (string, error) {
	selection, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{
		Title: "选择目标文件夹",
	})
	if err != nil {
		return "", err
	}
	return selection, nil
}

// OpenFile opens a file with the default application
func (a *App) OpenFile(path string) error {
	runtime.BrowserOpenURL(a.ctx, "file:///"+filepath.ToSlash(path))
	return nil
}

// ShowInExplorer shows the file in the system file explorer
func (a *App) ShowInExplorer(path string) error {
	runtime.BrowserOpenURL(a.ctx, "file:///"+filepath.ToSlash(filepath.Dir(path)))
	return nil
}

// buildFileTree recursively builds a file tree
func (a *App) buildFileTree(path string) (*FileNode, error) {
	return a.buildFileTreeWithDepth(path, 1)
}

func (a *App) buildFileTreeWithDepth(path string, maxDepth int) (*FileNode, error) {
	info, err := os.Stat(path)
	if err != nil {
		return nil, err
	}

	node := &FileNode{
		Name:    filepath.Base(path),
		Path:    path,
		IsDir:   info.IsDir(),
		Size:    info.Size(),
		ModTime: info.ModTime(),
	}

	if info.IsDir() && maxDepth > 0 {
		entries, err := os.ReadDir(path)
		if err != nil {
			return node, nil // Return node without children if can't read dir
		}

		for _, entry := range entries {
			childPath := filepath.Join(path, entry.Name())
			childNode, err := a.buildFileTreeWithDepth(childPath, maxDepth-1)
			if err == nil {
				node.Children = append(node.Children, childNode)
			}
		}
	}

	return node, nil
}

// GetDirectoryChildren loads children of a specific directory
func (a *App) GetDirectoryChildren(path string) ([]*FileNode, error) {
	info, err := os.Stat(path)
	if err != nil {
		return nil, err
	}

	if !info.IsDir() {
		return nil, fmt.Errorf("path is not a directory")
	}

	entries, err := os.ReadDir(path)
	if err != nil {
		return nil, err
	}

	var children []*FileNode
	for _, entry := range entries {
		childPath := filepath.Join(path, entry.Name())
		childInfo, err := entry.Info()
		if err != nil {
			continue
		}

		childNode := &FileNode{
			Name:    entry.Name(),
			Path:    childPath,
			IsDir:   entry.IsDir(),
			Size:    childInfo.Size(),
			ModTime: childInfo.ModTime(),
		}
		children = append(children, childNode)
	}

	return children, nil
}

// getDesktopPath returns the desktop path
func getDesktopPath() string {
	homeDir, err := os.UserHomeDir()
	if err != nil {
		return "."
	}
	return filepath.Join(homeDir, "Desktop")
}

// getConfigFilePath returns the configuration file path in temp directory
func (a *App) getConfigFilePath() string {
	tempDir := os.TempDir()
	configDir := filepath.Join(tempDir, "nova-search")
	
	// Ensure config directory exists
	os.MkdirAll(configDir, 0755)
	
	return filepath.Join(configDir, "config.json")
}

// loadConfig loads configuration from file
func (a *App) loadConfig() error {
	configPath := a.getConfigFilePath()
	data, err := os.ReadFile(configPath)
	if err != nil {
		return nil // Use default config if file doesn't exist
	}

	return json.Unmarshal(data, a.config)
}

// saveConfig saves configuration to file
func (a *App) saveConfig() error {
	configPath := a.getConfigFilePath()
	data, err := json.MarshalIndent(a.config, "", "  ")
	if err != nil {
		return err
	}

	return os.WriteFile(configPath, data, 0644)
}

// parseHotkeyString parses hotkey string like "Ctrl+Shift+F" into modifiers and key
func (a *App) parseHotkeyString(hotkeyStr string) ([]hotkey.Modifier, hotkey.Key, error) {
	parts := strings.Split(hotkeyStr, "+")
	if len(parts) < 2 {
		return nil, 0, fmt.Errorf("invalid hotkey format: %s", hotkeyStr)
	}

	var modifiers []hotkey.Modifier
	var key hotkey.Key

	// Parse modifiers and key
	for i, part := range parts {
		part = strings.TrimSpace(part)
		if i == len(parts)-1 {
			// Last part is the key
			switch strings.ToUpper(part) {
			case "F":
				key = hotkey.KeyF
			case "A":
				key = hotkey.KeyA
			case "B":
				key = hotkey.KeyB
			case "C":
				key = hotkey.KeyC
			case "D":
				key = hotkey.KeyD
			case "E":
				key = hotkey.KeyE
			case "G":
				key = hotkey.KeyG
			case "H":
				key = hotkey.KeyH
			case "I":
				key = hotkey.KeyI
			case "J":
				key = hotkey.KeyJ
			case "K":
				key = hotkey.KeyK
			case "L":
				key = hotkey.KeyL
			case "M":
				key = hotkey.KeyM
			case "N":
				key = hotkey.KeyN
			case "O":
				key = hotkey.KeyO
			case "P":
				key = hotkey.KeyP
			case "Q":
				key = hotkey.KeyQ
			case "R":
				key = hotkey.KeyR
			case "S":
				key = hotkey.KeyS
			case "T":
				key = hotkey.KeyT
			case "U":
				key = hotkey.KeyU
			case "V":
				key = hotkey.KeyV
			case "W":
				key = hotkey.KeyW
			case "X":
				key = hotkey.KeyX
			case "Y":
				key = hotkey.KeyY
			case "Z":
				key = hotkey.KeyZ
			case "SPACE":
				key = hotkey.KeySpace
			case "RETURN", "ENTER":
				key = hotkey.KeyReturn
			case "ESCAPE", "ESC":
				key = hotkey.KeyEscape
			case "TAB":
				key = hotkey.KeyTab
			case "F1":
				key = hotkey.KeyF1
			case "F2":
				key = hotkey.KeyF2
			case "F3":
				key = hotkey.KeyF3
			case "F4":
				key = hotkey.KeyF4
			case "F5":
				key = hotkey.KeyF5
			case "F6":
				key = hotkey.KeyF6
			case "F7":
				key = hotkey.KeyF7
			case "F8":
				key = hotkey.KeyF8
			case "F9":
				key = hotkey.KeyF9
			case "F10":
				key = hotkey.KeyF10
			case "F11":
				key = hotkey.KeyF11
			case "F12":
				key = hotkey.KeyF12
			default:
				return nil, 0, fmt.Errorf("unsupported key: %s", part)
			}
		} else {
			// Modifier
			switch strings.ToUpper(part) {
			case "CTRL", "CONTROL":
				modifiers = append(modifiers, hotkey.ModCtrl)
			case "SHIFT":
				modifiers = append(modifiers, hotkey.ModShift)
			case "ALT":
				modifiers = append(modifiers, hotkey.ModAlt)
			case "CMD", "COMMAND", "WIN", "WINDOWS":
				modifiers = append(modifiers, hotkey.ModAlt) // Use Alt as fallback for Windows key
			default:
				return nil, 0, fmt.Errorf("unsupported modifier: %s", part)
			}
		}
	}

	return modifiers, key, nil
}

// registerGlobalHotkey registers the global hotkey
func (a *App) registerGlobalHotkey() {
	if a.config.GlobalHotkey == "" {
		return
	}

	// Unregister existing hotkey if any
	if a.globalHotkey != nil {
		a.globalHotkey.Unregister()
		a.globalHotkey = nil
	}

	// Parse hotkey string
	modifiers, key, err := a.parseHotkeyString(a.config.GlobalHotkey)
	if err != nil {
		fmt.Printf("Failed to parse hotkey %s: %v\n", a.config.GlobalHotkey, err)
		return
	}

	// Create and register hotkey
	a.globalHotkey = hotkey.New(modifiers, key)
	err = a.globalHotkey.Register()
	if err != nil {
		fmt.Printf("Failed to register hotkey %s: %v\n", a.config.GlobalHotkey, err)
		a.globalHotkey = nil
		return
	}

	fmt.Printf("Global hotkey %s registered successfully\n", a.config.GlobalHotkey)

	// Start listening for hotkey events in a goroutine
	go a.handleHotkeyEvents()
}

// handleHotkeyEvents handles global hotkey events
func (a *App) handleHotkeyEvents() {
	if a.globalHotkey == nil {
		return
	}

	for {
		select {
		case <-a.globalHotkey.Keydown():
			// Toggle window visibility when hotkey is pressed
			a.toggleWindowVisibility()
		}
	}
}

// toggleWindowVisibility toggles the window visibility
func (a *App) toggleWindowVisibility() {
	if a.ctx == nil {
		return
	}

	// Always show and focus window when hotkey is pressed
	runtime.WindowShow(a.ctx)
	runtime.WindowCenter(a.ctx)
}

// initSystemTray initializes the system tray
func (a *App) initSystemTray() {
	go func() {
		systray.Run(a.onTrayReady, a.onTrayExit)
	}()
}

// onTrayReady is called when the system tray is ready
func (a *App) onTrayReady() {
	a.trayReady = true
	
	// Set tray icon (create a simple icon)
	iconData := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0xF3, 0xFF, 0x61}
	systray.SetIcon(iconData)
	systray.SetTitle("Nova Search")
	systray.SetTooltip("Nova Search - 文件搜索工具")
	
	// Create menu items
	showItem := systray.AddMenuItem("显示", "显示主窗口")
	systray.AddSeparator()
	exitItem := systray.AddMenuItem("退出", "退出程序")
	
	// Handle menu clicks
	showItem.Click(func() {
		runtime.WindowShow(a.ctx)
		runtime.WindowCenter(a.ctx)
	})
	
	exitItem.Click(func() {
		runtime.Quit(a.ctx)
	})
	
	// Handle tray icon click
	systray.SetOnClick(func(menu systray.IMenu) {
		runtime.WindowShow(a.ctx)
		runtime.WindowCenter(a.ctx)
	})
	
	// Handle right click to show menu
	systray.SetOnRClick(func(menu systray.IMenu) {
		menu.ShowMenu()
	})
}

// onTrayExit is called when the system tray exits
func (a *App) onTrayExit() {
	a.trayReady = false
}

// getCacheFilePath returns the cache file path for a given directory
func (a *App) getCacheFilePath(dirPath string) string {
	// Create a hash of the directory path to use as filename
	hash := md5.Sum([]byte(dirPath))
	hashStr := hex.EncodeToString(hash[:])
	
	// Get system temp directory
	tempDir := os.TempDir()
	cacheDir := filepath.Join(tempDir, "nova-search-cache")
	
	// Ensure cache directory exists
	os.MkdirAll(cacheDir, 0755)
	
	return filepath.Join(cacheDir, hashStr+".json")
}

// loadCachedFileTree loads file tree from cache
func (a *App) loadCachedFileTree(dirPath string) (*FileNode, error) {
	cacheFile := a.getCacheFilePath(dirPath)
	
	// Check if cache file exists
	if _, err := os.Stat(cacheFile); os.IsNotExist(err) {
		return nil, err
	}
	
	// Read cache file
	data, err := os.ReadFile(cacheFile)
	if err != nil {
		return nil, err
	}
	
	// Parse JSON
	var tree FileNode
	err = json.Unmarshal(data, &tree)
	if err != nil {
		return nil, err
	}
	
	return &tree, nil
}

// saveCachedFileTree saves file tree to cache
func (a *App) saveCachedFileTree(dirPath string, tree *FileNode) error {
	cacheFile := a.getCacheFilePath(dirPath)
	
	// Marshal to JSON
	data, err := json.MarshalIndent(tree, "", "  ")
	if err != nil {
		return err
	}
	
	// Write to cache file
	return os.WriteFile(cacheFile, data, 0644)
}

// clearCachedFileTree removes cached file tree
func (a *App) clearCachedFileTree(dirPath string) error {
	cacheFile := a.getCacheFilePath(dirPath)
	
	// Remove cache file if it exists
	if _, err := os.Stat(cacheFile); err == nil {
		return os.Remove(cacheFile)
	}
	
	return nil
}

// ClearAllCache clears all cached file trees
func (a *App) ClearAllCache() error {
	tempDir := os.TempDir()
	cacheDir := filepath.Join(tempDir, "nova-search-cache")
	
	// Remove entire cache directory
	return os.RemoveAll(cacheDir)
}
